Video Thumbnail
2:58
2:16
clock icon Created with Sketch. 2 minutes

Solution: Configuration


Alberto Miño

Hi Andreas, hope you're doing well!

Here is my solution:

*********** Code *************
from typing import Any, Callable
import json
from pathlib import Path
from functools import partial

import requests

HttpGet = Callable[[str], Any]

class ConfigFileMissing(Exception):
pass

class CityNotFoundError(Exception):
pass

def get(url: str) -> Any:
response = requests.get(url, timeout=5)
response.raise_for_status() # Raise an exception if the request failed
return response.json()

def get_forecast(http_get: HttpGet, url: str, api_key: str, city: str) -> dict[str, Any]:
url = url.format(city, api_key)
response = http_get(url)
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)
return response

def get_temperature(full_weather_forecast: dict[str, Any]) -> float:
temperature = full_weather_forecast["main"]["temp"]
return temperature - 273.15 # convert from Kelvin to Celsius

def get_humidity(full_weather_forecast: dict[str, Any]) -> int:
return full_weather_forecast["main"]["humidity"]

def get_wind_speed(full_weather_forecast: dict[str, Any]) -> float:
return full_weather_forecast["wind"]["speed"]

def get_wind_direction(full_weather_forecast: dict[str, Any]) -> int:
return full_weather_forecast["wind"]["deg"]

def main() -> None:
# Loading config object from json file
config_file_path = Path(".").absolute() / "config.json"

if not config_file_path.is_file():
raise ConfigFileMissing("Please check if config.json file is present.")

config = json.loads(config_file_path.read_text())

get_weather = partial(get_forecast, get, config["url"], config["api_key"])

city = config["city"]

weather_forecast = get_weather(city)

print(
f"The current temperature in {city} is {get_temperature(weather_forecast):.1f} °C."
)
print(f"The current humidity in {city} is {get_humidity(weather_forecast)}%.")
print(
f"The current wind speed in {city} is {get_wind_speed(weather_forecast) } m/s from direction {get_wind_direction(weather_forecast)} degrees."
)

if __name__ == "__main__":
main()
*********** EndCode *************

*********** config.json ***********
{
"url": "http://api.openweathermap.org/data/2.5/weather?q={}&appid={}",
"api_key": "123456",
"city": "Utrecht"
}
*********** End config.json *********

I could move api key to an .env file but to keep it simple in the exercise I just decided to keep it inside the json file.

Have a good day!
Alberto.

REPLY
Andreas [ArjanCodes Team]

Fair enough! As long as you are conscious about that decision. How do you like the challenges? :)

REPLY
Alberto Miño

Hi Andreas, yes, I really like the way we use the code version from the previous challenge in the next one to enhance functionality or apply a different design pattern.

REPLY
Andreas [ArjanCodes Team]

Glad to hear that Alberto! Feedback like that is precious for us. Could you please fill out the following form? It helps us improve the courses! :)

REPLY
Manuel Escalona

Since this code can work with several APIs, I also created the json file to setup several APIs. Here the code:

from typing import Any, Callable
from functools import partial
import requests
import json

API_NAME = "openweathermap"
CONFIG_FILE = "challenge_configurations/config/config.json"
HttpGet = Callable[[str], Any]

class CityNotFoundError(Exception):
pass

def get(url: str) -> Any:
response = requests.get(url, timeout=5)
response.raise_for_status() # Raise an exception if the request failed
return response.json()

def get_forecast(http_get: HttpGet, api_key: str, city: str, url: str) -> dict[str, Any]:
url = f"{url}?q={city}&appid={api_key}"
response = http_get(url)
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)
return response

def get_temperature(full_weather_forecast: dict[str, Any]) -> float:
temperature = full_weather_forecast["main"]["temp"]
return temperature - 273.15 # convert from Kelvin to Celsius

def get_humidity(full_weather_forecast: dict[str, Any]) -> int:
return full_weather_forecast["main"]["humidity"]

def get_wind_speed(full_weather_forecast: dict[str, Any]) -> float:
return full_weather_forecast["wind"]["speed"]

def get_wind_direction(full_weather_forecast: dict[str, Any]) -> int:
return full_weather_forecast["wind"]["deg"]

def main() -> None:
url:str = ''
api_key:str = ''
with open(CONFIG_FILE, "r") as f:
data = json.load(f)

for config in data['config_datails']:
if config['api_name'] == API_NAME:
api_key = config['api_key']
url = config['url']
break
else:
print('Invalid api name')
return

get_weather = partial(get_forecast, get, api_key)

city = "New York"

weather_forecast = get_weather(city, url)

print(
f"The current temperature in {city} is {get_temperature(weather_forecast):.1f} °C."
)
print(f"The current humidity in {city} is {get_humidity(weather_forecast)}%.")
print(
f"The current wind speed in {city} is {get_wind_speed(weather_forecast) } m/s from direction {get_wind_direction(weather_forecast)} degrees."
)

if __name__ == "__main__":
main()

REPLY
Andreas [ArjanCodes Team]

Nice solution! Just one question, how is the config_file setup?

REPLY
Manuel Escalona

This is the config.json file:

{
"config_datails": [
{
"api_name":"openweathermap",
"api_key":"767ee9774dccbca580fdd42f0c9a2c1c",
"url":"http://api.openweathermap.org/data/2.5/weather"
},
{
"api_name":"google_maps",
"api_key":"my_key_value",
"url":"https://googlemap.com/weather"
}
]

}

REPLY
Andreas [ArjanCodes Team]

Ok! I however would be careful storing keys in a json file, it is recommeded to use enviromental variables or a .env file. This is so we do not accidentally commit and publish is somewhere like GitHub.

Furthermore, if that is your API key for openweathermap, I would recommend you to remove that key immediately and use a new one

REPLY
Manuel Escalona

Yes, I agree with you Andreas. The API was deleted and a new one created.

Thanks

REPLY
Andreas [ArjanCodes Team]

Good! No worries!

REPLY
Philipp Walter

Hi Arjan,
I like that solution specifing a url_template_string including a placeholder for the city.

As you mentioned at 0:40 I would prefer to store the API key in the .env to avoid an accidentially upload of the api_key to github.

It is already mentioned in the comments below, that pydantic will deprecate some methods like parse_raw, thats why I try to create some generic functions to save and load pydantic_based jsonfiles :-):

def save_pydantic_model(file_path: str, pydantic_model: BaseModel) -> None:
with open(file_path, "w") as file:
file.write(pydantic_model.model_dump_json())

T = TypeVar("T", bound=BaseModel)

def load_pydantic_model(file_path: str, pydantic_model: Type[T]) -> T:
with open(file_path, "r") as file:
return pydantic_model.model_validate_json(file.read())

REPLY
Andreas [ArjanCodes Team]

There are more benefits as well to storing the API key as an environment variable because it isolates critical application configuration and lets other services access the variables as well (as long as they are on the same machine of course).

Nice solution! I would consider using the new version of generics also to make the code a bit more sleek! We released a Tuesday Tip about it not long ago: Python 3.12 Generics in a Nutshell

REPLY
Philipp Walter

Hi Andreas,
thanks for the feedback and your hint regarding Generics in 3.12.
Unfortunately I have to stay in 3.11 in my daily work because of a package a package being not valid for 3.12 :-(.
That's why I am happy that your challenges specify 3.11 in the .toml :-D so I can train the "old" handling of generics.

PS: Regarding your feedback for "Day_13" of course def main(): is missing, thanks for the hint.

REPLY
Andreas [ArjanCodes Team]

I understand, hopefully your daily work gets upgraded to 3.12 :D

REPLY
Juan José Expósito González

I used python-decouple to read from configuration files (.env) and for the rest, my solution is not as clean as the proposed one is:

rom typing import Any, Callable
from functools import partial
import json
import requests

CONFIG_FILE = "config.json"
with open(CONFIG_FILE, encoding="UTF_8") as f:
config = json.load(f)

HttpGet = Callable[[str], Any]

class CityNotFoundError(Exception):
pass

def get(url: str) -> Any:
response = requests.get(url, timeout=5)
response.raise_for_status() # Raise an exception if the request failed
return response.json()

def get_forecast(http_get: HttpGet, api_key: str, city: str) -> dict[str, Any]:
api_url = config["API_URL"]
url = f"{api_url}?q={city}&appid={api_key}"
response = http_get(url)
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)
return response

def get_temperature(full_weather_forecast: dict[str, Any]) -> float:
temperature = full_weather_forecast["main"]["temp"]
return temperature - 273.15 # convert from Kelvin to Celsius

def get_humidity(full_weather_forecast: dict[str, Any]) -> int:
return full_weather_forecast["main"]["humidity"]

def get_wind_speed(full_weather_forecast: dict[str, Any]) -> float:
return full_weather_forecast["wind"]["speed"]

def get_wind_direction(full_weather_forecast: dict[str, Any]) -> int:
return full_weather_forecast["wind"]["deg"]

def main() -> None:
api_key = config["API_KEY"]
for city in config["cities"]:
get_weather = partial(get_forecast, get, api_key)

weather_forecast = get_weather(city)
temperature = get_temperature(weather_forecast)
humidity = get_humidity(weather_forecast)
wind_speed = get_wind_speed(weather_forecast)
wind_direction = get_wind_direction(weather_forecast)

print(f"The current temperature in {city} is {temperature:.1f} °C.")
print(f"The current humidity in {city} is {humidity}%.")
print(
f"The current wind speed in {city} is {wind_speed} m/s from direction {wind_direction} degrees."
)

if __name__ == "__main__":
main()

REPLY
David Nevin

I'm a big fan of dotenv, nice to see a json alternative

from dotenv import load_dotenv

# Load all envronment variables
load_dotenv()

API_KEY: str = os.getenv("API_KEY", "UnKnown")

REPLY
Arjan Egges

Me too - I do that all the time. I wanted to keep the exercises simple and standalone, so I haven't used dotenv a lot in my courses.

REPLY
Show More